# 作用域与
这一部分会说明作用域相关的基本概念, 然后牵出一个 JS 中非常重要的概念作用域链, 并且简单描述一下 ES6 中的块级作用域.
警告
关于 JS 的执行环境和作用域, 我认为他们就是一个东西, 反正我没看出什么区别, 底下的内容可能存在执行环境和作用域混用的情况, 知道是一个东西就行.
# 作用域的相关概念
# 总览
不像其他的编程语言使用 {}
来限制作用域, JS 中的作用域有两类: 全局作用域和函数作用域. 在 JS 中, 每个作用域中都可能有大量的属性(变量)和一些子作用域(函数作用域), 那么就需要一个专门的对象来保存这些数据, 这在 JS 中被定义为变量对象.
TIP
用函数来标识作用域, emmmmmm . 后面会出现很多与之相关的非常诡异的现象.
现在我们保存了数据, 但是还不够. 如果我们的执行流从上级作用域 A 切换到下级作用域 B 中, 总有一天, 我们要离开 B , 那么我们如何让解释器想起来 B 的上一级作用域是 A ?
必然的, 我们需要一个什么东西来管理这些执行环境, 在操作系统中, 这种事情常常交给队列或栈来实现. 在 JS 中也可以使用栈的概念, 定义为作用域栈, 作用域栈的顶部当前执行流所在的作用域, 当执行流离开此作用域时, 这个作用域就会被弹出.
这里, 作用域链, 在我的理解, 就是一种对作用域栈的具体实现. 作用域栈的每个节点保存的是作用域; 相应的, 作用域链中的每个节点保存的是变量对象的引用.
有些时候, 我们可能还会看到活动对象的概念, 活动对象就是作用域链上正在被执行和引用的变量对象, 其实是同一个东西在不同时刻的叫法.
TIP
执行环境, 变量对象, 活动对象, 作用域链都是在 JS 代码执行过程中出现变化的.
WARNING
关于活动对象的描述可能和 JS 高级程序设计
书中的描述不太一致, 我个人认为, 书的作者或者译者在这个地方串用了活动对象和变量对象. 笔记中采用的说法是网络说法, 我觉得比较有道理, 故使用此说法.
到这个位置, 和作用域的相关的基本概念就处理完了, 下面算是小结吧.
# 执行环境 / 作用域
执行环境规定了变量 / 对象能够访问其他数据的范围. 一切的一切从全局执行环境开始. 每个函数都会有自己的执行环境.
# 变量对象
变量对象储存了执行执行环境中包含的变量和函数, 每个执行环境都会有一个自己的变量对象.
# 活动对象
本质上也是变量对象, 但是为了表达代码运行的动态性, 提出了活动对象的概念. 当有某个变量对象变成活动对象时, 这意味代码就执行到这个变量对象对应的作用域.
# 作用域栈
保证执行环境中的所有变量和函数都能够被有序访问. 和上面的说的一样, 我更倾向于他是一个高级抽象概念, 用于解释作用域链的.
# 作用域链
保证执行环境中的所有变量和函数都能够被有序访问. 作用域链中包含着许多变量对象的引用, 在作用域链活跃的一头, 就是活动对象的引用.
TIP
每个作用域在被执行的时候, 都会拥有自己的作用域链, 大家的作用域链可能会有一些不同, 或长或短.
# 父子作用域的访问限制
父环境无法通过作用域链向下(由父到子)访问到子环境中的变量;
子环境可以通过作用域链向上(由子到父)访问到父环境中的变量;
# 作用域链中变量的查询 (变量提升的问题)
如果作用域链中存在多个同名的局部变量, JS 在使用 此变量名的变量 时就会启动变量查询机制, 这个机制将会从当前的执行环境开始, 沿着作用域链向上查找最近的声明了的同名变量.
var a = 1;
function test() {
console.log(a);
var a = 12;
}
test(); // 执行 test , console 将打印出 undefinded .
2
3
4
5
6
上面的例子, 就说明了这点. 直觉上我们可能会觉得应该打印 1 , 但是 test
函数的作用域的变量对象包含了一个 a
变量. 所以变量名的查询机制不会翻看 1 行的变量 a
.
然而, JS 解释器按照顺序执行, 看到 3 行要打印的内容并没有被赋值, 于是 undefinded 和一个大写的 NAIVE 就出现在了浏览器屏幕上.
这种情况被称为变量提升, 即变量可以在声明前使用的情况.
# 块级作用域
之前说过, 早版本的 JS 中 {}
不具备对作用域的限制功能, 但是 ES6 就和大多数编程语言一样, 支持 {}
的作用域. 由于没有块级作用域, 很容易出现上面提到的变量提升和 for 循环中循环索引泄露的问题.
那么这里就会有两个主题, 一个是如何使用 JS 仿造块级作用域, 二是简单 BB 一下 ES6 中的块级作用域.
# JS 仿造块级作用域
在没有块级作用域的年代, 一种特殊的写法, 叫 匿名立即执行函数表达式 ,
// 如果我们这样写 for 循环, 5 行的 log 是能够打印出数值 10 的 (最后 i 被 ++ 了)
for (var i = 0; i < 10; i++) {
console.log(i)
}
console.log(i); // 10
// 如果我们使用 匿名立即执行函数表达式 , 13 行的 log 将报错
(function() {
for (var i = 0; i < 10; i++) {
console.log(i)
}
})();
console.log(i); // Can't find variable: i
2
3
4
5
6
7
8
9
10
11
12
13
一个比较特殊的地方就是 12 行高亮位置的 ()
, 它允许当前这个匿名函数立即执行, 从而达到伪块级作用域的效果
# ES6 的块级作用域
先看一下 ES6 中提到的 var
和 let
. ES6 中块级作用域的基础内容从直觉上看和其他的编程语言没有太大的差别.
TIP
当 ES6 的解释器遇到 let
和 const
两个关键字声明的变量 / 常量时, 仍然会和遇到 var
一样, 先将变量 / 常量提升到块级作用域的开头. 只不过, var
允许 变量使用代码 在 变量声明代码 之前, 而 let
和 const
在遇到 变量使用代码 在 变量声明代码 之前的情况会报错.
# 循环索引
循环中声明的循环索引算哪一部分的块级作用域.
我们可以做出三种假设:
- 属于 for 循环所在的 "高级" 的作用域
- 属于 for 循环
{}
内的作用域 - 自成一家
针对假设编写代码, 如果 1 和 2 的假设不成立, 则可以认为 3 成立.
// 第 1 种假设的代码
let i= 22;
for (let i = 0; i < 10; i++) {
console.log(i); // 这里将打印 0 到 9
}
2
3
4
5
第一段代码正常运行, 说明循环索引 i 和全局下的 i 并不矛盾, 假设 1 不成立.
// 第 2 种假设的代码
for (let i = 0; i < 10; i++) {
let i = 12;
console.log(i); // 这里将打印 10 个 12
}
2
3
4
5
第二段代码也是正常运行, 说明循环索引 i 和 for 循环块级作用域中的 i 并不矛盾, 假设 2 不成立.
上面的两段代码的实验结果更加倾向于第 3 种假设, 即循环中声明的循环索引自成一家.
TIP
两段代码不要同时运行.
# 块级作用域与函数声明
函数能否在块级作用域中被声明, 由于历史原因, 这个问题变得非常的复杂. 非常有意思的一点, 这里 ES 想做出一些规范, 但是浏览器总是有一些自己的想法.
ES5 规定, 函数只能在顶层作用域和函数作用域之中声明, 不能在块级作用域声明. 但是浏览器没有遵守这个规定, 为了兼容以前的旧代码, 还是支持在块级作用域之中声明函数,
ES6 规定, 明确允许在块级作用域之中声明函数. 同时在块级作用域之中, 函数声明语句的行为类似于 let , 在块级作用域之外不可引用; 但是 ES6 在附录 B 里面规定, 浏览器的实现可以不遵守上面的规定. 函数声明将类似于 var .
总结来说, 最大的坑是在块级作用域中使用函数声明式的写法, 可能不同浏览器在不同环境下的行为都不太一致, 标准在这个地方就和虚设的一样.
# 在块级作用域中编写函数的建议
努力避免在块级作用域中使用函数声明式的写法, 应该使用函数表达式.